using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net;
using System.IO;
using System.Threading;
using System.Text;
using System.IO.Compression;
using System.Configuration;
using WebProxyApplication.Blocked;
using WebProxyApplication.Certificate;
using WebProxyApplication.FormCheck;
using WebProxyApplication.WebCache;

namespace WebProxyApplication
{
    /// <summary>
    /// Summary description for ProxyHandler
    /// </summary>
    public class ProxyHandler : IHttpHandler
    {
        private static readonly int MinAllowedPageRank = Convert.ToInt32(ConfigurationManager.AppSettings["MinAllowedPageRank"]);
        private static readonly int WeightPhishIP = Convert.ToInt32(ConfigurationManager.AppSettings["WeightPhishIP"]);
        private static readonly int WeightPhishURL = Convert.ToInt32(ConfigurationManager.AppSettings["WeightPhishURL"]);
        private static readonly int WeightPageRank = Convert.ToInt32(ConfigurationManager.AppSettings["WeightPageRank"]);
        private static readonly int SecurityThreshold = Convert.ToInt32(ConfigurationManager.AppSettings["SecurityThreshold"]);

        private enum ProxyUrlHandlingBehavior
        {
            UnKnownUrl,
            KnownGoodUrl,
            KnownUnsafeUrl
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Corrects the port number.
        /// </summary>
        /// <param name="orig"></param>
        /// <returns></returns>
        public Uri UriToPassThrough(HttpRequest requestToProxy)
        {
            int newPort;
            UriBuilder newUri = new UriBuilder(requestToProxy.Url);
            string host = requestToProxy.ServerVariables["HTTP_HOST"];

            // set default value
            if (requestToProxy.IsSecureConnection)
            {
                newPort = 443;
            }
            else
            {
                newPort = 80;
            }

            if (!String.IsNullOrEmpty(host))
            {
                if (host.Contains(":"))
                {
                    newPort = Convert.ToInt32(host.Substring(host.IndexOf(":") + 1));
                }
            }

            newUri.Port = newPort;
            return newUri.Uri;
        }

        public void ProcessRequest(HttpContext context)
        {
            BlockedReason urlBlockedReason;
            HttpResponse responseFromProxy = context.Response;
            HttpRequest requestToProxy = context.Request;
            Uri requestedUri;
            ProxyUrlHandlingBehavior urlHandlingBehavior;
            int urlRating = 0;

            try
            {
                requestedUri = UriToPassThrough(requestToProxy);
                ProxyLogger.Log.InfoFormat("Request received for {0}", requestedUri);

                urlHandlingBehavior = GetHandlingBehaviorForUri(requestedUri, out urlBlockedReason, out urlRating);
                switch (urlHandlingBehavior)
                {
                    case ProxyUrlHandlingBehavior.UnKnownUrl:
                        ProcessUnknownUriRequest(requestToProxy, responseFromProxy, requestedUri);
                        break;
                    case ProxyUrlHandlingBehavior.KnownGoodUrl:
                        ProcessDefaultRequest(requestToProxy, responseFromProxy, requestedUri);
                        break;
                    case ProxyUrlHandlingBehavior.KnownUnsafeUrl:
                    default:
                        BlockUriRequest(context, urlBlockedReason);
                        break;
                }

                responseFromProxy.End();
            }
            catch (UrlBlockedException ex)
            {
                BlockUriRequest(context, ex.UrlBlockedReason, ex.UrlBlockedPageRank);
            }
            catch (ThreadAbortException ex)
            {
                throw ex;
            }
            catch (Exception ex)
            {
                ProxyLogger.Log.Error(ex);
            }
        }

        private void BlockUriRequest(HttpContext context, BlockedReason urlBlockedReason, Int32 pageRank=0)
        {
            // just incase, clearing the response.
            context.Response.Clear();
            context.Response.ClearHeaders();

            ProxyLogger.Log.WarnFormat("Blocking uri {0} reason {1}", context.Request.Url, urlBlockedReason);
            context.Server.TransferRequest(String.Format("~/Blocked/Blocked.aspx?blockCode={0}&PageRank={1}", (int)urlBlockedReason, pageRank));
        }

        private void ProcessUnknownUriRequest(HttpRequest requestToProxy, HttpResponse responseFromProxy, Uri requestedUri)
        {
            //Uri requestedUri = new Uri(requestToProxy.Url.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped));
            HttpWebRequest webRequest;
            HttpWebResponse serverResponse = null;
            string mimeType;
            string html;
            BlockedReason blockedReason;

            //requestedUri = new Uri("https://www.silversneakers.com/");

            webRequest = GetPreparedWebRequest(requestToProxy, requestedUri);

            try
            {
                serverResponse = (HttpWebResponse)webRequest.GetResponse();
            }
            catch (WebException wex)
            {
                if (wex.Status.HasFlag(WebExceptionStatus.TrustFailure))
                {
                    // this occurs when the certificate isn't trusted.
                    // for this situation bubble up a new exception
                    UrlBlockedException certificateException = new UrlBlockedException("Trust failure occurred.", wex);
                    certificateException.UrlBlockedReason = BlockedReason.InvalidCertificate;
                    throw certificateException;
                }
                ProxyLogger.Log.Error(String.Format("WebException occurred for uri {1} with status {0}", wex.Status, requestedUri), wex);
                serverResponse = (HttpWebResponse)wex.Response;
            }
            catch (Exception ex)
            {
                ProxyLogger.Log.Error(ex);
                responseFromProxy.StatusCode = 500;
                responseFromProxy.StatusDescription = "Unknown error";
                responseFromProxy.Write("An error occurred.");
                responseFromProxy.End();
                return; // don't think this line will ever get called
            }

            mimeType = serverResponse.ContentType;
            if (String.IsNullOrEmpty(mimeType))
            {
                mimeType = MimeTypeHelper.DetermineMimeTypeFromFileExtension(serverResponse.ResponseUri);
            }
            ProxyLogger.Log.InfoFormat("Mime type is {0} for uri {1}", mimeType, requestedUri);
            if (mimeType.StartsWith("text/html"))
            {
                html = GetResponseContent(serverResponse);
                if (HtmlChecker.IsHtmlSafe(html, out blockedReason))
                {
                    // add request to cache
                    WebCacheUtil.SaveAsSafe(requestedUri);
                }
                else
                {
                    UrlBlockedException urlBlockedException = new UrlBlockedException();
                    urlBlockedException.UrlBlockedReason = blockedReason;
                    throw urlBlockedException;
                }

                CopyHttpHeader(responseFromProxy, serverResponse, ConfigurationManager.AppSettings["headerSkipProxyReturnList"]);
                responseFromProxy.ContentType = mimeType;
                responseFromProxy.Write(html);
            }
            else
            {
                // do we want to save other than html types?  If so, un-comment
                // WebCacheUtil.SaveAsSafe(requestedUri);
                SendResponseFromProxy(responseFromProxy, serverResponse);
            }
        }

        private void ProcessDefaultRequest(HttpRequest requestToProxy, HttpResponse responseFromProxy, Uri requestedUri)
        {
            //Uri requestedUri = new Uri(requestToProxy.Url.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped));
            WebResponse serverResponse = null;
            WebRequest webRequest;

            webRequest = GetPreparedWebRequest(requestToProxy, requestedUri);

            try
            {
                serverResponse = webRequest.GetResponse();
            }
            catch (WebException wex)
            {
                if (wex.Status.HasFlag(WebExceptionStatus.TrustFailure))
                {
                    // this occurs when the certificate isn't trusted.
                    // for this situation bubble up a new exception
                    UrlBlockedException certificateException = new UrlBlockedException("Trust failure occurred.", wex);
                    certificateException.UrlBlockedReason = BlockedReason.InvalidCertificate;
                    throw certificateException;
                }
                ProxyLogger.Log.Error(String.Format("WebException occurred for uri {1} with status {0}", wex.Status, requestedUri), wex);
                serverResponse = wex.Response;
            }
            catch (Exception ex)
            {
                ProxyLogger.Log.Error(ex);
                responseFromProxy.StatusCode = 500;
                responseFromProxy.StatusDescription = "Unknown error";
                responseFromProxy.End();
                return; // don't think this line will ever get called
            }

            if (serverResponse == null)
            {
                responseFromProxy.End();
                return;
            }

            SendResponseFromProxy(responseFromProxy, (HttpWebResponse)serverResponse);
        }

        private HttpWebRequest GetPreparedWebRequest(HttpRequest requestToProxy, Uri requestedUri)
        {
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(requestedUri);
            byte[] buffer;
            Stream destinationStream;
            int chunk;

            webRequest.Method = requestToProxy.HttpMethod;

            // pass header values along to webRequest
            CopyHttpHeaders(requestToProxy, webRequest);
            // see section 14.44 in http://www.ietf.org/rfc/rfc2068.txt
            webRequest.Headers.Add("Via", String.Format("{0} edu_asu_WebProxyApplication", requestToProxy.ServerVariables["SERVER_PROTOCOL"].Replace("HTTP/", String.Empty)));

            if (requestToProxy.ContentLength > 0)
            {
                buffer = new byte[2048];
                using (Stream srcStream = requestToProxy.InputStream)
                {
                    destinationStream = webRequest.GetRequestStream();

                    while ((chunk = srcStream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        destinationStream.Write(buffer, 0, chunk);
                    }
                }
            }

            if (webRequest.Headers.Count == 0)
                ProxyLogger.Log.Debug("No headers");
            else
            {
                foreach (string key in webRequest.Headers)
                {
                    ProxyLogger.Log.DebugFormat("Request Header {0}={1}", key, requestToProxy.Headers[key]);
                }
            }

            return webRequest;
        }

        private string GetResponseContent(HttpWebResponse response)
        {
            Encoding encoding;
            string content;

            encoding = Encoding.GetEncoding(response.CharacterSet);
            ProxyLogger.Log.InfoFormat("ContentEncoding:{0}, CharSet:{1}({3}), ContentLength:{2}", response.ContentEncoding, response.CharacterSet, response.ContentLength, encoding);

            using (Stream srcStream = response.GetResponseStream())
            {
                if (response.ContentEncoding.ToLower().Contains("gzip"))
                {
                    using (Stream compressionSteam = new GZipStream(srcStream, CompressionMode.Decompress))
                    {
                        using (StreamReader sr = new StreamReader(compressionSteam, encoding))
                        {
                            content = sr.ReadToEnd();
                        }
                    }
                }
                else if (response.ContentEncoding.ToLower().Contains("deflate"))
                {
                    using (Stream compressionSteam = new DeflateStream(srcStream, CompressionMode.Decompress))
                    {
                        using (StreamReader sr = new StreamReader(compressionSteam, encoding))
                        {
                            content = sr.ReadToEnd();
                        }
                    }
                }
                else
                {
                    using (StreamReader sr = new StreamReader(srcStream, encoding))
                    {
                        content = sr.ReadToEnd();
                    }
                }
            }

            return content;
        }

        private void SendResponseFromProxy(HttpResponse responseFromProxy, HttpWebResponse serverResponse)
        {
            byte[] buffer;
            Stream destinationStream;
            int chunk;
            
            buffer = new byte[2048];
            CopyHttpHeader(responseFromProxy, serverResponse, ConfigurationManager.AppSettings["headerSkipProxyReturnListForKnownGood"]);

            if (String.IsNullOrEmpty(serverResponse.ContentType))
            {
                responseFromProxy.ContentType = MimeTypeHelper.DetermineMimeTypeFromFileExtension(serverResponse.ResponseUri);
            }
            else
            {
                responseFromProxy.ContentType = serverResponse.ContentType;
            }

            responseFromProxy.StatusCode = (int)serverResponse.StatusCode;

            using (Stream srcStream = serverResponse.GetResponseStream())
            {
                destinationStream = responseFromProxy.OutputStream;

                if (serverResponse.ContentEncoding.ToLower().Contains("gzip"))
                {
                    using (Stream compressionSteam = new GZipStream(srcStream, CompressionMode.Decompress))
                    {
                        while ((chunk = compressionSteam.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            destinationStream.Write(buffer, 0, chunk);
                        }
                    }
                }
                else if (serverResponse.ContentEncoding.ToLower().Contains("deflate"))
                {
                    using (Stream compressionSteam = new DeflateStream(srcStream, CompressionMode.Decompress))
                    {
                        while ((chunk = compressionSteam.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            destinationStream.Write(buffer, 0, chunk);
                        }
                    }
                }
                while ((chunk = srcStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    destinationStream.Write(buffer, 0, chunk);
                }
            }
            serverResponse.Close();
        }

        private void CopyHttpHeader(HttpResponse responseFromProxy, WebResponse serverResponse, string headerSkipProxyReturnList = "")
        {
            string[] headerSkipProxyReturnListArray;
            string headerValue;

            headerSkipProxyReturnListArray = headerSkipProxyReturnList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            responseFromProxy.Clear();
            responseFromProxy.ClearHeaders();

            foreach (string headerKey in serverResponse.Headers)
            {
                if (!headerSkipProxyReturnListArray.Contains(headerKey))
                {
                    headerValue = serverResponse.Headers[headerKey];
                    responseFromProxy.Headers.Add(headerKey, headerValue);
                }
            }
        }

        private void CopyHttpHeaders(HttpRequest requestToProxy, HttpWebRequest webRequest)
        {
            string headerValue;
            System.Net.Cookie convertedCookie;
            System.Web.HttpCookie cookie;
            
            foreach (string headerKey in requestToProxy.Headers)
            {
                // Example header entries
                // Accept=text/javascript, application/javascript, */*; q=0.01
                // Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.7
                // Accept-Encoding=gzip, deflate
                // Accept-Language=en-us,en;q=0.5
                // Cookie=__utma=1.380414486.1331746340.1331746340.1331746340.1; __utmb=1.5.10.1331746340; __utmc=1; __utmz=1.1331746340.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); StyleSheetWidget_TextSizeWidget=MediumText; .ASPXANONYMOUS=28B5kJo4zQEkAAAAMWU5MDM2YzctNzAzYy00Yjc4LWFmOTQtOTIxYmJjNzdjZjg50; language=en-US
                // Host=www.silversneakers.com
                // Referer=http://www.silversneakers.com/
                // User-Agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:8.0.1) Gecko/20100101 Firefox/8.0.1
                // Proxy-Connection=keep-alive
                // X-Requested-With=XMLHttpRequest
                headerValue = requestToProxy.Headers[headerKey];
                switch (headerKey)
                {
                    case "Accept":
                        webRequest.Accept = headerValue;
                        break;
                    case "Connection":
                        if (headerValue == "Close")
                            webRequest.KeepAlive = false;
                        else
                            webRequest.KeepAlive = true;
                        //ProxyLogger.Log.DebugFormat("Connection: {0}", webRequest.Connection);
                        break;
                    case "Content-Length":
                        webRequest.ContentLength = long.Parse(headerValue);
                        break;
                    case "Content-Type":
                        if (headerValue != "0")
                        {
                            webRequest.ContentType = headerValue;
                        }
                        break;
                    case "Expect":
                        webRequest.Expect = headerValue;
                        break;
                    case "Date":
                        webRequest.Date = DateTime.Parse(headerValue);
                        break;
                    case "Host":
                        webRequest.Host = headerValue;
                        break;
                    case "If-Modified-Since":
                        webRequest.IfModifiedSince = DateTime.Parse(headerValue);
                        break;
                    case "Range":
                        throw new NotImplementedException(String.Format("Header Range has not been implemented at this time ({0})", headerValue));
                        //webRequest.AddRange(
                        //break;
                    case "Referer":
                        webRequest.Referer = headerValue;
                        break;
                    case "Transfer-Encoding":
                        webRequest.TransferEncoding = headerValue;
                        break;
                    case "User-Agent":
                        webRequest.UserAgent = headerValue;
                        break;
                    case "Proxy-Connection":
                        // don't copy
                        break;
                    case "Cookie":
                        if (webRequest.CookieContainer == null)
                        {
                            webRequest.CookieContainer = new CookieContainer();
                        }
                        foreach (string cookieKey in requestToProxy.Cookies)
                        {
                            cookie = requestToProxy.Cookies[cookieKey];
                            convertedCookie = new Cookie();
                            if (!String.IsNullOrEmpty(cookie.Domain))
                                convertedCookie.Domain = cookie.Domain;
                            else
                                convertedCookie.Domain = requestToProxy.Url.Host;
                            convertedCookie.Expires = cookie.Expires;
                            convertedCookie.Name = cookie.Name;
                            convertedCookie.Value = cookie.Value;
                            convertedCookie.HttpOnly = cookie.HttpOnly;
                            if (!String.IsNullOrEmpty(cookie.Path))
                                convertedCookie.Path = cookie.Path;
                            webRequest.CookieContainer.Add(convertedCookie);
                        }
                        break;
                    default:
                        webRequest.Headers.Add(headerKey, headerValue);
                        break;
                }
                ProxyLogger.Log.DebugFormat("Copying header {0}: {1}", headerKey, headerValue);
            }
        }

        private ProxyUrlHandlingBehavior GetHandlingBehaviorForUri(Uri requestedUri, out BlockedReason urlBlockedReason, out int rating)
        {
            string urlToCheck = requestedUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port & ~UriComponents.Query, UriFormat.UriEscaped);
            ProxyUrlHandlingBehavior behavior = ProxyUrlHandlingBehavior.UnKnownUrl;
            IPAddress[] addrArray;
            int pageRankValue = -1;
            rating = 10; // initial value. negative factors will reduce this below the acceptable threshold.

            urlBlockedReason = BlockedReason.Unknown;

            addrArray = Dns.GetHostAddresses(requestedUri.GetComponents(UriComponents.Host, UriFormat.UriEscaped));
            if (MinAllowedPageRank != -1) //check if PageRank checking enabled
                pageRankValue = Utils.getPageRank(urlToCheck);
            if (WebCacheUtil.IsKnownSafe(requestedUri))
            {
                behavior = ProxyUrlHandlingBehavior.KnownGoodUrl;
            }
            if (pageRankValue != -1 && pageRankValue <= MinAllowedPageRank)
            {
                rating += (pageRankValue + WeightPageRank);
                urlBlockedReason = BlockedReason.LowPageRank;
            }
            if (Utils.IsPhishIPAddress(addrArray))
            {
                rating += WeightPhishIP;
                urlBlockedReason = BlockedReason.PhishingSite;
            }
            if ( Utils.isPhishUrl(urlToCheck))
            {
                rating += WeightPhishURL;
                urlBlockedReason = BlockedReason.PhishingSite;
            }
            // evaluate calculated security vs the acceptable threshold          
            if (rating < SecurityThreshold)
            {
                behavior = ProxyUrlHandlingBehavior.KnownUnsafeUrl;
                if (urlBlockedReason == BlockedReason.LowPageRank)
                {
                    UrlBlockedException ex = new UrlBlockedException();
                    ex.UrlBlockedPageRank = pageRankValue;
                    ex.UrlBlockedReason = BlockedReason.LowPageRank;
                    throw ex;
                }
            }

            return behavior;
        }
    }
}